#!/usr/bin/python
# $Id$
# $Author$
# $LastChangedDate$
# $LastChangedRevision$
# $LastChangedBy$
#
#######################################
#
#	      Build.py v. 0.3beta
#
# Copyright (c) 2008 Michal Hordecki
#    Licensed under GNU LGPL
#
#######################################

"""
Build.py

Licensed under GNU LGPL.
@group Internal: Private _*
"""

from optparse import OptionParser
import sys 
import os
import shutil
import pickle
import hashlib


def _get_compiler(name):
	if name=='g++':
		return Compiler_generic('g++')

def _get_linker(name):
	if name=='g++':
		return Compiler_generic('g++',objonlyprefix='')

class Dependency():
	pkgconfig=[]
	lib=[]
	include=[]
	
	def check(self):
		return True
	def cflags(self):
		if len(self.pkgconfig)==0: return ''
		pkgc='`pkg-config --cflags '
		for i in self.pkgconfig:
			pkgc+=i+' '
		pkgc+='` '
		return [pkgc]
	
	def ldflags(self):
		if len(self.pkgconfig)==0: return ''
		pkgc='`pkg-config --libs '
		for i in self.pkgconfig:
			pkgc+=i+' '
		pkgc+='` '
		return [pkgc]

class Option(object):
	"""
	Global singleton. It acts as a global storage for user options.
	
	Preferred usage is:
		>>> Option().key=value # Setter
		>>> print Option().key # Getter

	If you want to check Option object against existence of an option, use:
		>>> if 'key' in Option()
	"""
	def __new__(self):
		if not '_instance' in self.__dict__:
			self._instance = object.__new__(self)
		return self._instance
	def __contains__(self,x):
		if x in dir(self):
			return True
		else:	return False

class Private(object):
	"""
	Global singleton. Only for internal use. Technically, it works exactly as Option object.
	"""
	def __new__(self):
		if not '_instance' in self.__dict__:
			self._instance = object.__new__(self)
		return self._instance
	def __contains__(self,x):
		if x in dir(self):
			return True
		else:	return False

class Log(object):
	"""
	Global singleton. It acts as a global logging facility. It's used to cache
	compiled files checksums and installation logs. Logs usually go to file named
	.buildpy.log

	Log object is in fact modified version of Option object with a few added methods.
	"""
	data={}
	def __new__(self):
		if not '_instance' in self.__dict__:
			self._instance = object.__new__(self)
			self._instance.load()
		return self._instance
	def save(self):
		"""Saves logs to log file"""
		f=open('.buildpy.log','w+')
		pickle.dump(self.data,f)
		f.close()
	def load(self):
		"""Loads logs from log file."""
		if not os.path.exists('.buildpy.log'):
			return
		f=open('.buildpy.log')
		self.__dict__['data']=pickle.load(f)
		f.close()
	def reset(self):
		"""Resets log data"""
		self.data={}
	def __setattr__(self,attr,key,*unexpected):
		self.data[attr]=key
	def __getattr__(self,attr,*unexpected):
		return self.data[attr]
	def __contains__(self,x):
		if x in self.data:
			return True
		else:	return False

class Compiler_generic:
	inputprefix=''
	outputprefix='-o'
	objonlyprefix='-c'
	name=''
	def __init__(self,name,inputprefix='',outputprefix='-o',objonlyprefix='-c'):
		self.inputprefix=inputprefix
		self.outputprefix=outputprefix
		self.objonlyprefix=objonlyprefix
		self.name=name

	def compile(self,input,output,flags):
		cmdstr=self.name+' '+self.objonlyprefix+' '+flags+' '
		for i in input:
			cmdstr+=self.inputprefix+i+' '
		cmdstr+=self.outputprefix+output
		print cmdstr
		return os.system(cmdstr)




class Progress(object):
	actions=[]
	title=''
	def __new__(self):
		if not '_instance' in self.__dict__:
			self._instance = object.__new__(self)

		return self._instance
	def reset(self):
		self.actions=[]
		self.title=[]
	def add(self,action,title):
		self.actions.append([action,title])
	def process(self):
		actual=1
		count=len(self.actions)
		print self.title
		for a in self.actions:
			print '('+str(actual)+'/'+str(count)+') ',
			print a[1]
			eval(a[0][0])(self,a[0][1])
			actual+=1
		print 'Finished:',self.title
	Log().save()
		
def copy(s,dest):
	for d in Private().files[s]:
		destdir=os.path.join(dest,os.path.split(d)[0])
		Progress().add(['_copy',[d,destdir]],'Installing '+d)
def _copy(p,params):
	d=params[0]
	destdir=params[1]
	if not os.path.exists(destdir):
		os.makedirs(destdir)
	cmd='install '+d+' '+destdir+os.sep
	print cmd
	Log().installed.append(os.path.join(destdir,os.path.basename(d)))
	if os.system(cmd)!=0:
#		Log().save()
#		error('Unknown error encountered, stopping.')
		pass

def copy_bin(dest='',symlink=''):
	Progress().add(['_copy_bin',[dest,symlink]],'Installing binaries')

def _copy_bin(p,params):
	dest=params[0]
	symlink=params[1]
	if dest=='':
		dest=Option().bindir
	cmd='install '+Option().binname+' '+dest+os.sep
	Log().installed.append(os.path.join(dest,Option().package))
	print cmd
	if os.system(cmd)!=0:
#		Log().save()
#		error('Unknown error encountered, stopping.')
		pass
	if symlink!='':
		cmd='ln -s '+dest+os.sep+Option().package+' '+symlink+os.sep+Option().package
		Log().installed.append(os.path.join(symlink,Option().package))
		print cmd
		if os.system(cmd)!=0:
			#Log().save()
			#error('Unknown error encountered, stopping.')
			pass

def init_install(title=''):
	Progress().reset()
	Progress().title=title

def process_install():
	Progress().process()


#### BUILDFILE MACROS
Option().actual=''
Option().extensions=[]
Option().compiler=''
Option().linker=''
Option().dependencies=Dependency()
Option().prefix='/usr/'
Option().sharedir=os.path.join(Option().prefix,'share')
Option().bindir=os.path.join(Option().prefix,'bin')
Option().cflags=[]
Option().ldflags=[]

Private().files={}
Private().data={}
if not 'source_files' in Log():
	Log().source_files={}
if not 'installed' in Log():
	Log().installed=[]

def package(p):
	c='package_'+p
	Option().package=p
	eval(c)()
def target(t):
	c=Option().package+'_'+t
	eval(c)()
def _file_calc_hash(p):
	f=open(p)
	return hashlib.md5(f.read()).hexdigest()
def add_dir(c,d):
	"""
	Scans recursively given directory, searching for files. If category is set
	to 'source', it will only seek for extensions in Option().extensions.
	@type c: string
	@type d: string
	@param c: category
	@param d: directory to search in
	"""
	if not os.path.exists(d):
		warning('Given path '+d+' does not exist.')
	if not c in Private().files:
		  Private().files[c]=[]
	if c=='source':
		for root, dirs, files in os.walk(d):
			for file in files:
				if root.find('/.')!=-1: continue #checking against hidden files
				ext=os.path.splitext(file)[1][1:] #file extension
				if not ext in Option().extensions:
					print 'Found ',root+os.sep+file,'- ommitting'
				else:
					print 'Found ',root+os.sep+file
					Private().files[c].append(os.path.join(root,file))
	else:
		for root, dirs, files in os.walk(d):
			for file in files:
				if root.find('/.')!=-1: continue #checking against hidden files
				print 'Found '+c+' file',root+os.sep+file
				Private().files[c].append(os.path.join(root,file))

def process_build():
	"""
	Launches build procedure. Iterates though all files visited by add_dir
	which are tagged as 'source', compiles them and then links. Binary is named
	equally as the package and it's placed in main project directory.
	"""
	Option().dependencies.check()
	Private().Compiler=_get_compiler(Option().compiler)
	Private().Linker=_get_linker(Option().linker)
	Private().objs=[]
	Private().cflags=str.join(' ',Option().dependencies.cflags())+' '+str.join(' ',Option().cflags)
	Private().ldflags=str.join(' ',Option().dependencies.ldflags())+' '+str.join(' ',Option().ldflags)
	if not 'binname' in Option():
		  Option().binname=Option().package
	Progress().reset()
	#compiling
	
	for f in Private().files['source']:
		objpath=os.path.splitext(f)[0]+'.o '
		if f in Log().source_files:
			  if Log().source_files[f]==_file_calc_hash(f):
				    Private().objs.append(objpath)
				    continue
		if Private().Compiler.compile([f],objpath,Private().cflags)!=0:
			  Log().save()
			  error('Build error,stopping.')
		Log().source_files[f]=_file_calc_hash(f)
		Private().objs.append(objpath)

	Log().save()

	#linking

	print 'Linking..'
	print Private().objs	
	if Private().Linker.compile(Private().objs,Option().binname,Private().ldflags)!=0:
		  error('Linking_error, stoppping.')
	
	print 'Building finished.'

		
def _recursive_del(d):
	"""
	Recursively deletes given directory.
	@type d: string
	@param d: directory
	"""
	if not os.path.exists(d): return
	for root, dirs, files in os.walk(d, topdown=False):
		for name in files:
			os.remove(os.path.join(root, name))
		for name in dirs:
			os.rmdir(os.path.join(root, name))
	os.rmdir(d)


#### BUILD.PY CODE

def die(text):
	print text
	exit()
def error(text):
	print 'ERROR :',text
	exit()
def warning(text):
	print 'WARNING :',text



opts=OptionParser(version='%prog 0.1', usage="%prog [TARGET] [OPTIONS]", description='Simple build tool written in Python. Useful in small projects where overbloated features of make, jam & co are unnecessary. build.py uses external configuration files called buildfiles, similar to Makefiles and Jamfiles. See docs for more info.')

opts.set_defaults(file='Buildfile')
opts.add_option('-f','--file',dest='file',help='set buildfile path. Default : \'buildfile\'')

"""opts.add_option('-G','--Get',action='store_const',const='G',dest='mode',help='Get mangas from server')
opts.add_option('-Q','--Query',action='store_const',const='Q',dest='mode',help='Query info about mangas from server')
opts.add_option('-m','--manga',dest='manga',help='Select manga which has NAME as a keyword (See also -Q option)',metavar='NAME')
opts.add_option('-c','--chapter',dest='chapter',help='Query/get manga from chapter specified by NUM',metavar="NUM")
opts.add_option('-r','--range',dest='range',help='Query/get range of chapters specified by RANGE (format: START-END)',metavar="RANGE")
opts.add_option('-a','--all',action="store_true",dest='all',help='Query/get all chapters of manga')
"""
TARGETS=[]

opt=opts.parse_args(sys.argv[1:])
opt=opt[0]

for a in sys.argv[1:]:
	if a[0]=='-': break
	TARGETS.append(a)

if len(TARGETS)==0:
	TARGETS=['build']

if not os.path.exists(opt.file):
	error('Buildfile '+opt.file+' does not exist.')

execfile(opt.file)
package(Option().default_package)
for t in TARGETS:
	target(t)
